﻿using System;
using System.ComponentModel;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;
using System.Linq;
using System.Reflection;

namespace Dino.EntityFramework
{
    /// <summary>
    /// Provides a base set of functionality using the DbContext from EntityFramework with some extensions for more friendly usage
    /// </summary>
    public abstract class ObjectContext : DbContext, IObjectContext
    {
        private readonly Func<Type, bool> _isTypeRegistered;
        private readonly Func<Type, object> _resolveType;

        protected ObjectContext()
            : this(f => false, f => f)
        {
        }

        protected ObjectContext(string nameOrConnectionString)
            : this(nameOrConnectionString, f => false, f => f)
        {
        }

        protected ObjectContext(Func<Type, bool> isTypeRegistered, Func<Type, object> resolveType)
        {
            _isTypeRegistered = isTypeRegistered;
            _resolveType = resolveType;
            Configuration.LazyLoadingEnabled = true;
            ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.ObjectStateManagerChanged += ObjectStateManagerChanged;
        }

        protected ObjectContext(string nameOrConnectionString, Func<Type, bool> isTypeRegistered, Func<Type, object> resolveType)
            : base(nameOrConnectionString)
        {
            _isTypeRegistered = isTypeRegistered;
            _resolveType = resolveType;
            Configuration.LazyLoadingEnabled = true;
            ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.ObjectStateManagerChanged += ObjectStateManagerChanged;
        }

        private void ObjectStateManagerChanged(object sender, CollectionChangeEventArgs e)
        {
            if (e.Action == CollectionChangeAction.Add && ((ObjectStateManager)sender).GetObjectStateEntry(e.Element).State == EntityState.Unchanged)
            {
                OnEntityMaterialized(e.Element, sender as ObjectStateManager);
            }
        }

        private void OnEntityMaterialized(object entity, ObjectStateManager objectStateManager)
        {
            SetEntityRemovedEvent(entity, objectStateManager);

            foreach (var propertyInfo in entity.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (_isTypeRegistered(propertyInfo.PropertyType))
                {
                    propertyInfo.SetValue(entity, _resolveType(propertyInfo.PropertyType), null);
                }
            }
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }

        private void SetEntityRemovedEvent(object entity, ObjectStateManager objectStateManager)
        {
            if (IsImplemented(entity.GetType(), typeof(ICollectionEntityRemoved)))
            {
                var relatedEnd = objectStateManager.GetRelationshipManager(entity).GetAllRelatedEnds().FirstOrDefault();
                if (relatedEnd != null)
                {
                    var objectQuery = relatedEnd.CreateSourceQuery() as ObjectQuery;
                    if (objectQuery != null)
                    {
                        if (objectQuery.Context != null)
                        {
                            ((ICollectionEntityRemoved)entity).EntityRemoved += objectQuery.Context.DeleteObject;
                        }
                    }
                }
            }
        }

        private static bool IsImplemented(Type objectType, Type interfaceType)
        {
            return objectType.GetInterfaces().Any(i => i == interfaceType);
        }

        public void Commit()
        {
            base.SaveChanges();
        }

        public IQueryable<T> Query<T>() where T : class
        {
            return Set<T>();
        }

        public void Add<T>(T entity) where T : class
        {
            Set<T>().Add(entity);
        }

        public void Delete<T>(T entity) where T : class
        {
            Set<T>().Remove(entity);
        }
    }
}
